macro: Validate custom handler signatures and clarify dispatch errors#19
Open
macro: Validate custom handler signatures and clarify dispatch errors#19
Conversation
HDauven
approved these changes
Apr 16, 2026
Member
HDauven
left a comment
There was a problem hiding this comment.
LGTM, but there's two edge cases I think we need to handle. I'll leave it up to you to decide whether they're follow-ups or not.
b8a5129 to
64abc32
Compare
Custom handlers registered via `#[contract(encode_input = "…")]`, `#[contract(decode_input = "…")]`, or `#[contract(decode_output = "…")]` are moved verbatim into the generated `data_driver` submodule and called positionally by the dispatch match arms. A signature that doesn't match what the dispatch site expects would previously produce a cryptic downstream type error against code the user didn't write. The macro now compile-time-checks handler signatures and emits a clear `compile_error!` at the handler definition, naming the handler, the role, and the full expected signature. Comparison runs the user's argument / return types through the contract module's import map and token-compares against the canonical per-role shape, so idiomatic short paths (`Vec<u8>`, `Error`, `JsonValue` after a `use`) are accepted against `alloc::vec::Vec<u8>` / `dusk_data_driver::Error` / `dusk_data_driver::JsonValue`. `'static` lifetimes on reference arguments or in return types are rejected with a dedicated diagnostic — the generated dispatcher passes a local borrow the handler can't bind, so a `'static` promise would surface as a lifetime mismatch deep in macro-generated code. Elided and handler-generic lifetimes (`fn handler<'a>(… &'a …)`) continue to be accepted. Canonical per-role signatures are factored into `data_driver.rs` as `handler_signature` / `role_name` / `handler_signature_display` so the validator and the code that calls handlers can't drift apart. Trybuild fixtures pin the diagnostics for: - wrong argument type (encode_input), - wrong return type (decode_input), - wrong argument count (decode_output), - `&'static str` rejection.
The three data-driver dispatch sites (`encode_input_fn`, `decode_input_fn`,
`decode_output_fn`) share the same runtime path for functions marked
`is_custom`: when no handler is registered for the relevant role, the
match arm returns `Error::Unsupported`. Previously all three sites
emitted an identical `"custom handler required: {fn_name}"` message,
leaving the user to figure out which of the three roles they'd missed
and what signature the replacement needed.
The three sites now share a `missing_handler_arm` helper that pulls the
role name and canonical handler signature from the per-role helpers
introduced in the previous commit. Each site produces:
missing <role> handler for `<fn>`; expected handler signature: <sig>
A user seeing this at runtime can identify both which role they
missed and the exact shape of the handler they need to register —
no trawling through macro-generated code required.
Custom data-driver handlers are spliced verbatim from the contract
module into the generated `data_driver` submodule — but the submodule
only preluded `alloc::{format, string::String, vec::Vec}`, nothing from
the user's outer `use` items. A handler written with idiomatic short
paths would fail to compile after expansion even though the validator
accepted it:
use dusk_data_driver::{Error, JsonValue};
#[contract(decode_output = "get_value")]
fn decode_value(bytes: &[u8]) -> Result<JsonValue, Error> { … }
// → error[E0425]: cannot find type `JsonValue` in this scope
Re-emit the contract module's `use` items inside the generated
submodule so each spliced handler sees the same imports it did at its
original site — signature *and* body. A handler body like
`.map_err(Error::from)` now resolves just as it did in the outer
module.
Only imports a handler actually references are carried over, detected
by scanning each handler's tokens for identifier appearances. This
keeps contract-only imports (e.g. `types::Ownable` gated behind the
`abi` feature in the test-contract) from leaking into the data-driver
build, where their feature gate wouldn't be satisfied.
The submodule scaffolding drops its `use alloc::{format, string::String,
vec::Vec};` prelude and inlines the fully-qualified paths at their use
sites instead, so a user-land `use alloc::vec::Vec;` in the contract
module doesn't collide with a preluded `Vec` in the submodule.
A `compile-pass-short-paths` trybuild fixture and a
`short_paths_compile_pass` integration test pin the round-trip: a
contract with short-path handlers must round-trip through the macro
and compile. This is the specific failure mode Defect 3 exposed —
validator-only coverage missed it precisely because the splicer was
silently broken.
…overage The previous canonical-path signatures (`alloc::vec::Vec<u8>`, `dusk_data_driver::Error`, `dusk_data_driver::JsonValue`) side-stepped the re-emit pipeline — the short paths are the idiomatic Rust a user would actually write after a `use`, and the only way to catch a regression in handler-import re-emit is to exercise that form. `Vec`, `Error`, and `JsonValue` are now imported at the top of the contract module, gated on the `data-driver` feature (no contract-side code references the short names, so the gate keeps the contract build warning-free). The `#[contract]` macro detects which imports handlers reference and splices only those into the generated `data_driver` submodule.
64abc32 to
b626bee
Compare
HDauven
approved these changes
Apr 17, 2026
Member
HDauven
left a comment
There was a problem hiding this comment.
LGTM
There's a remaining edge case around trait imports which the current reemit filter can still miss when moving into the data_driver but I'll handle that in a follow-up PR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two coupled fixes to the
contract-macro/diagnostic surface around custom data-driver handlers. Supersedes task 189 (re-scope).1. Compile-time handler signature validation
Handler functions registered via
#[contract(encode_input = "…")],decode_input, ordecode_outputare spliced into the generateddata_drivermodule and called directly by the dispatch match arms. A handler with the wrong shape (argument count, argument type, return type) used to slip past the macro and fail downstream with a cryptic type error against code the user didn't write.Adds a validator that runs over each extracted handler and emits a clear
compile_error!at the handler's definition naming the handler, the role, and the expected signature. Types are compared structurally —Vec<u8>(afteruse alloc::vec::Vec) orError(afteruse dusk_data_driver::Error) are accepted, whilefoo::ErrorandMyErrorare rejected.2. Role-specific runtime errors at the three dispatch sites
The three data-driver dispatch sites (
encode_input_fn,decode_input_fn,decode_output_fn) used to return the same vague"custom handler required: {fn}"when a method marked#[contract(custom)]was dispatched without a matching handler. The user couldn't tell which of the three roles they were missing a handler for, nor the signature that role expects.Each site now emits a role-tailored message that names the role and the expected handler signature in concrete types, so the reader can fix the handler from the error alone.
Single source of truth
Both the validator and the dispatch error messages pull the canonical per-role signature from
data_driver::handler_signature, so the two can't drift apart.Tests
validate::custom_handler— positive regression per role plus negatives for wrong arg count, wrong arg type, wrong return type, missing return, self receiver,foo::Errorprefix,MyErrorlast segment,&mut strmutability.data_driverunit tests updated (not deleted) to assert on the new role-specific error content.Vec<u8>,Error,JsonValue,&'static str).test-contractintegration tests (2 handlers, encode_input + decode_output) still pass — no regression for existing users.